package org.jpropelleralt.jview.node.impl;

import java.awt.BorderLayout;
import java.awt.LayoutManager;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

import org.jpropelleralt.box.Box;
import org.jpropelleralt.box.Boxes;
import org.jpropelleralt.change.Change;
import org.jpropelleralt.jview.JView;
import org.jpropelleralt.jview.JViews;
import org.jpropelleralt.jview.factory.JViewFactory;
import org.jpropelleralt.node.Node;
import org.jpropelleralt.path.Path;
import org.jpropelleralt.path.impl.PathBuilder;
import org.jpropelleralt.ref.Ref;
import org.jpropelleralt.ref.impl.PathRef;
import org.jpropelleralt.ref.impl.RefDefault;
import org.jpropelleralt.view.View;
import org.jpropelleralt.view.Views;
import org.jpropelleralt.view.update.UpdateManager;

import com.jgoodies.forms.builder.DefaultFormBuilder;
import com.jgoodies.forms.layout.FormLayout;

/**
 * View of the {@link Ref}s contained in a {@link Node}
 */
public class NodeView implements JView {

	private final Ref<? extends Node> value;
	private final Ref<Boolean> locked;
	private final LinkingJPanel panel;
	private final UpdateManager updateManager;

	//The views used for each Ref of current Node, indexed by Ref name
	private Map<String, JView> subViews = new HashMap<String, JView>();

	//The classes of each Ref of current Node, indexed by Ref name
	private List<Class<?>> subViewClasses = new LinkedList<Class<?>>();
	
	//The view names in order we will display them
	private List<String> subViewNames = new LinkedList<String>();
	
	private final List<String> displayedNames;
	
	private Node nodeUsedForUI = null;
	
	private final JViewFactory factory;
	
	private final Runnable updateRunnable;
	
	private NodeView(Ref<? extends Node> value, Ref<Boolean> locked, UpdateManager updateManager, JViewFactory factory, List<String> displayedNames) {
		
		this.displayedNames = displayedNames;
		this.value = value;
		this.locked = locked;
		this.factory = factory;
		
		panel = new LinkingJPanel(this, new BorderLayout());

		this.updateManager = updateManager;
		updateManager.registerUpdatable(this);

		value.features().addView(this);
		if (locked != null) {
			locked.features().addView(this);
		}
		
		updateRunnable = new Runnable() {
			@Override
			public void run() {
				internalUpdate();
			}
		};
		
		//Initial update
		internalUpdate();
	}

	@Override
	public void update() {
		//Update later, in case we need to create new refs
		SwingUtilities.invokeLater(updateRunnable);
	}
	
	//Complicated suppression here - basically we use valueClass throughout
	//to ensure that there is a common type "T" between Refs and Classes,
	//but we can't communicate this to the somewhat unpleasant Java type system.
	//Hence we just need to use raw types.
	@SuppressWarnings({ "unchecked", "rawtypes" })
	private void internalUpdate() {
		Node newValue = value.get();
		
		//The "old" value is the one we actually used to build the UI
		Node oldValue = nodeUsedForUI;
		
		//If we have not changed value instance, nothing to do
		if (oldValue == newValue) return;

		//This makes sure that we minimise the name/editability/class check below - 
		// even if we don't rebuild the UI, we DO remember the model as being
		//that used for UI, so if the bean has a deep change we don't recheck
		//props, we just recognise that we are still viewing the same instance.
		nodeUsedForUI = newValue;
		
		//Find the ref names of the new value that have a concrete value class
		List<String> newSubViewNames = new LinkedList<String>();
		List<Class<?>> newSubViewClasses = new LinkedList<Class<?>>();
		if (newValue != null) {
			
			//If we have displayedNames list, iterate it, otherwise iterate all names in
			//the new value
			Iterable<String> currentDisplayedNames = displayedNames;
			if (currentDisplayedNames == null) {
				currentDisplayedNames = newValue;
			}
			
			for (String name : currentDisplayedNames) {
				Ref<?> ref = newValue.get(name);
				Class<?> valueClass = ref.valueClass();
				if (valueClass != null) {
					newSubViewNames.add(name);
					newSubViewClasses.add(valueClass);
				}
			}
		}

		//If the old and new values have the same class, and
		//the lists of displayed names and classes are the same as current ones,
		//then we don't need to alter displayed views
		if (sameClass(oldValue, newValue) && newSubViewNames.equals(subViewNames) && newSubViewClasses.equals(subViewClasses)) {
			return;
		}
		
		//Find the new subviews we will use
		Map<String, JView> newSubViews = new HashMap<String, JView>();
		
		//If we have a non-null new value, get views from factory
		if (newValue != null) {
			for (int i = 0; i < newSubViewNames.size(); i++) {
				String newName = newSubViewNames.get(i);
				Class<?> newClass = newSubViewClasses.get(i);
				String localisedName = Boxes.localisedName(newValue.getClass(), newName);
				
				//First we need to make a PathRef to get to the correct data - since this
				//starts from our value, it will update when our value updates, even if our
				//value gets a new instance - this is important since we won't regenerate
				//our views unless we get a new instance of a different class (or with
				//different ref names/classes)
				Path path = PathBuilder.create(value).to(newClass, newName);
				Ref newRef = PathRef.create((Class)newClass, path);
				
				//Note that we create the view using our own model, so it will always
				//(try to) display our current model
				JView newView = factory.viewFor((Class)newClass, (Ref)newRef, locked, RefDefault.create(localisedName));
				if (newView!=null) {
					newSubViews.put(newName, newView);
				}
			}
		}
		
		//Use the new views, names and classes
		subViews = newSubViews;
		subViewNames = newSubViewNames;
		subViewClasses = newSubViewClasses;
		
		//Rebuild using new views
		rebuild();
	}

	@Override
	public void changes(Map<Box, Change> changes) {
		//OPTIMISATION, around 10% - the alternative to the method implementation
		//below is just to always call updateManager.updateRequiredBy(view);
		//This optimisation will break if any events have incorrect deep/shallow status
		
		//We only require an update on a shallow change to value - that is,
		//when a new value is set, NOT when one of the Refs of value
		//changes. When a Ref of value changes, it will be displayed by a
		//sub view update
		
		//First see if the value has changed at all
		if (changes.containsKey(value)) {
			//Only update if the value has a change to any instances
			if (changes.get(value).shallow()) {
				updateManager.updateRequiredBy(this);
			}
		}
		
		//This is the unoptimised code - always update
		//updateManager.updateRequiredBy(this);
	}

	private void rebuild() {
		
		//logger.finest("rebuild()");
		
		FormLayout layout = new FormLayout(Views.getStandard3ColumnDefinition());
		
		DefaultFormBuilder builder = new DefaultFormBuilder(layout);

		Node node = value.get();

		// Fill the grid with components
		for (String name : subViewNames) {

			JView view = subViews.get(name);
			JComponent component = view.getComponent();

			String labelString = view.selfNaming() ? "" : Boxes.localisedName(node.getClass(), name) + ":";
			
			builder.append(labelString, component);
		}

		int[] rowGroup = new int[subViewNames.size()];
		for (int i = 0; i < rowGroup.length; i++) {
			rowGroup[i] = 2*i+1;
		}
		builder.getLayout().setRowGroups(new int[][]{rowGroup});

		//Use the new panel in this editor
		panel.removeAll();
		panel.add(builder.getPanel());
		panel.revalidate();

		//Store the bean we just used - this is used on update to
		//check whether we have changed model instance, and might
		//hence need to rebuild again etc.
		nodeUsedForUI = node;
		
	}
	
	@Override
	public Format format() {
		return Format.LARGE;
	}

	@Override
	public JComponent getComponent() {
		return panel;
	}

	@Override
	public boolean selfNaming() {
		return true;
	}
	
	/**
	 * Return true if a and b are the same class, or are both null
	 * @param a
	 * 		first object
	 * @param b
	 * 		second object
	 * @return
	 * 		True if a and b are same class, or both null
	 */
	private static boolean sameClass(Object a, Object b) {
		if (a == null) {
			return (b == null);
		} else if (b == null) {
			return false;
		} else {
			return a.getClass().equals(b.getClass());
		}
	}
	
	/**
	 * Create a {@link NodeView}
	 * @param value				The {@link Node} to display, as a {@link Ref} 
	 * @param locked			While this is true, view will not permit editing (control disabled) 
	 * @param updateManager		The {@link UpdateManager} to use
	 * @param factory			The {@link JViewFactory} to use 
	 * @param displayedNames	The names to display, in order, or null to display all refs in the current {@link Node},
	 * 							in the order returned by that Node.
	 * @return					A new {@link NodeView}
	 */
	public static NodeView create (Ref<? extends Node> value, Ref<Boolean> locked, UpdateManager updateManager, JViewFactory factory, List<String> displayedNames) {
		return new NodeView(value, locked, updateManager, factory, displayedNames);
	}

	/**
	 * Create a {@link NodeView}
	 * @param value				The {@link Node} to display, as a {@link Ref} 
	 * @param locked			While this is true, view will not permit editing (control disabled) 
	 * @param factory			The {@link JViewFactory} to use 
	 * @param displayedNames	The names to display, in order, or null to display all refs in the current {@link Node},
	 * 							in the order returned by that Node.
	 * @return					A new {@link NodeView}
	 */
	public static NodeView create (Ref<? extends Node> value, Ref<Boolean> locked, JViewFactory factory, List<String> displayedNames) {
		return new NodeView(value, locked, Views.updateManager(), factory, displayedNames);
	}
	
	/**
	 * Create a {@link NodeView}
	 * @param value				The {@link Node} to display, as a {@link Ref} 
	 * @param locked			While this is true, view will not permit editing (control disabled) 
	 * @param displayedNames	The names to display, in order, or null to display all refs in the current {@link Node},
	 * 							in the order returned by that Node.
	 * @return					A new {@link NodeView}
	 */
	public static NodeView create (Ref<? extends Node> value, Ref<Boolean> locked, List<String> displayedNames) {
		return new NodeView(value, locked, Views.updateManager(), JViews.getJViewFactory(), displayedNames);
	}
	
	/**
	 * Create a {@link NodeView}
	 * @param value				The {@link Node} to display, as a {@link Ref} 
	 * @param locked			While this is true, view will not permit editing (control disabled) 
	 * @param displayedNames	The names to display, in order, or null to display all refs in the current {@link Node},
	 * 							in the order returned by that Node.
	 * @return					A new {@link NodeView}
	 */
	public static NodeView create (Ref<? extends Node> value, Ref<Boolean> locked,String... displayedNames) {
		return new NodeView(value, locked, Views.updateManager(), JViews.getJViewFactory(), Arrays.asList(displayedNames));
	}
	
	/**
	 * Create a {@link NodeView} displaying all {@link Ref}s in each {@link Node}
	 * @param value				The {@link Node} to display, as a {@link Ref} 
	 * @param locked			While this is true, view will not permit editing (control disabled) 
	 * @return					A new {@link NodeView}
	 */
	public static NodeView create (Ref<? extends Node> value, Ref<Boolean> locked) {
		return new NodeView(value, locked, Views.updateManager(), JViews.getJViewFactory(), null);
	}
	
	/**
	 * Create a {@link NodeView}
	 * @param value				The {@link Node} to display, as a {@link Ref} 
	 * @param displayedNames	The names to display, in order, or null to display all refs in the current {@link Node},
	 * 							in the order returned by that Node.
	 * @return					A new {@link NodeView}
	 */
	public static NodeView create (Ref<? extends Node> value, List<String> displayedNames) {
		return new NodeView(value, null, Views.updateManager(), JViews.getJViewFactory(), displayedNames);
	}
	
	/**
	 * Create a {@link NodeView}
	 * @param value				The {@link Node} to display, as a {@link Ref} 
	 * @param displayedNames	The names to display, in order, or null to display all refs in the current {@link Node},
	 * 							in the order returned by that Node.
	 * @return					A new {@link NodeView}
	 */
	public static NodeView create (Ref<? extends Node> value, String... displayedNames) {
		return new NodeView(value, null, Views.updateManager(), JViews.getJViewFactory(), Arrays.asList(displayedNames));
	}
	
	/**
	 * Create a {@link NodeView} displaying all {@link Ref}s in each {@link Node}
	 * @param value				The {@link Node} to display, as a {@link Ref} 
	 * @return					A new {@link NodeView}
	 */
	public static NodeView create (Ref<? extends Node> value) {
		return new NodeView(value, null, Views.updateManager(), JViews.getJViewFactory(), null);
	}

	/**
	 * This is the same as the parent {@link JComponent}, 
	 * the only difference is that it remembers its {@link JView}, 
	 * so that as long as the {@link JComponent} is in
	 * use, the {@link JView} survives as well. Otherwise, the 
	 * {@link JView} can be GCed while the {@link JComponent} is still 
	 * in use, stopping the {@link JComponent} being updated.
	 * Otherwise, it is very easy for the {@link JView} to be collected,
	 * since a {@link View} is NOT kept alive by the data it is
	 * watching, and is generally not of much interest after its
	 * component has been added to a UI. 
	 */
	public static class LinkingJPanel extends JPanel {
		private final JView owner;
		private LinkingJPanel(JView owner, LayoutManager layout) {
			super(layout);
			this.owner = owner;
		}
		/**
		 * Get the owner {@link JView}
		 * @return	The owner of this component
		 */
		public JView owner() {
			return owner;
		}
	}
}
